package com.hcl.orm.core.executor;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.hcl.orm.core.annotation.UK;
import com.hcl.orm.core.dialect.SqlDefinition;
import com.hcl.orm.core.dialect.SqlMetadata;
import com.hcl.orm.core.dialect.Signature;
import com.hcl.orm.core.executor.page.PageParam;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
 * @author hucl
 * @projectName hcl-orm
 * @description: TODO
 * @date 2021/8/1117:08
 */
public class JdbcFormatter {
    /**
     * 获取参数字符串的正则表达式
     */
    private static final String PARAM_PATTERN = "(\\#\\{)([\\w?]+)(\\})";

    public static JdbcParam format(SqlDefinition sqlDefinition, Object[] args) {
        final Signature signature = sqlDefinition.getSignature();
        final SqlMetadata sqlMetadata = sqlDefinition.getSelectedSqlMetadata();
        // 对于自定义SQL的情况，先支持基本类型 多参数方式
        String[] selectives = StrUtil.subBetweenAll(sqlMetadata.getRawSql(), "[", "]");
        Arrays.stream(selectives).forEach(s -> {
            List<Integer> paramIndexs = Arrays.stream(StrUtil.subBetweenAll(s, "#{", "}"))
                    .filter(i -> i.startsWith("??")).map(i -> {
                        return Integer.parseInt(StrUtil.strip(i, "??"));
                    }).collect(Collectors.toList());
            AtomicBoolean isSelectiveValid = new AtomicBoolean(true);
            paramIndexs.forEach(pi -> {
                Integer argIndex = sqlMetadata.getParamsIndexMap().get(pi);
                if (args[argIndex] == null) {
                    isSelectiveValid.set(false);
                    return;
                }
            });
            if (!isSelectiveValid.get()) {
                paramIndexs.forEach(pi -> {
                    sqlMetadata.getParamsIndexMap().remove(pi);
                });
                sqlDefinition.setDialectSql(sqlDefinition.getDialectSql().replace(s, ""));
                if (StrUtil.isNotEmpty(sqlDefinition.getPageQueryCountSql())) {
                    sqlDefinition.setPageQueryCountSql(sqlDefinition.getPageQueryCountSql().replace(s, ""));
                }
            }
        });

        sqlDefinition.setDialectSql(sqlDefinition.getDialectSql()
                .replaceAll("\\[", "")
                .replaceAll("\\]", "")
                .replaceAll(PARAM_PATTERN, "?"));


        Object[] realArgs = sqlMetadata.getParamsIndexMap().values().stream().map(index -> args[index]).toArray();
        JdbcParam jdbcParam = new JdbcParam(sqlDefinition.getDialectSql(),
                realArgs);

        if (signature.isReturnsPage()) {
            // 增加page处理
            sqlDefinition.setPageQueryCountSql(sqlDefinition.getPageQueryCountSql()
                    .replaceAll("\\[", "")
                    .replaceAll("\\]", "")
                    .replaceAll(PARAM_PATTERN, "?"));

            PageParam pageParam = new PageParam(1, 100);
            if (signature.getPageParamIndex() != null) {
                pageParam = signature.extractPageParam(args);
            }
            jdbcParam.setPage(true);
            jdbcParam.setCountSqlTmpl(sqlDefinition.getPageQueryCountSql());
            jdbcParam.setPageParam(pageParam);
            jdbcParam.setJdbcPageArgs(new Object[]{(pageParam.getPageNo() - 1) * pageParam.getPageSize(), pageParam.getPageSize()});
        }
        return jdbcParam;
    }

    /**
     * 格式化单表的操作SQL语句
     *
     * @param sqlDefinition
     * @param args
     * @return
     */
    public static JdbcParam simpleInsertFormat(SqlDefinition sqlDefinition, Object[] args) {
        List<Object> sqlArgs = new ArrayList<>();
        String strFields = null;
        final Signature signature = sqlDefinition.getSignature();
        final SqlMetadata sqlMetadata = sqlDefinition.getSelectedSqlMetadata();
        Object paramObj = convertArgsToSqlCommandParam(signature.getParams(), signature.getMethod(), args);
        Field[] fields = ClassUtil.getDeclaredFields(paramObj.getClass());
        if (sqlMetadata.isSelective()) {
            strFields = Arrays.stream(fields).filter(f -> {
                Object param = ReflectUtil.getFieldValue(paramObj, f);
                if (param != null) {
                    sqlArgs.add(param);
                    return true;
                } else {
                    return false;
                }
            }).map(x -> StrUtil.toUnderlineCase(x.getName()))
                    .collect(Collectors.joining(","));
        } else {
            strFields = Arrays.stream(fields).map(f -> {
                        sqlArgs.add(ReflectUtil.getFieldValue(paramObj, f));
                        return StrUtil.toUnderlineCase(f.getName());
                    }
            ).collect(Collectors.joining(","));
        }
        return new JdbcParam(StrUtil.format(sqlDefinition.getDialectSql(), strFields, getPlaceHolder(sqlArgs.size()))
                , ArrayUtil.toArray(sqlArgs, Object.class));
    }

    public static JdbcParam simpleBatchInsertFormat(SqlDefinition sqlDefinition, Object[] args) {
        final Signature signature = sqlDefinition.getSignature();
        Field[] fields = ClassUtil.getDeclaredFields(signature.getInputRealType());
        Collection params = (Collection) convertArgsToSqlCommandParam(signature.getParams(), signature.getMethod(), args);
        String strFields = Arrays.stream(fields).map(x -> StrUtil.toUnderlineCase(x.getName()))
                .collect(Collectors.joining(","));

        List<Object[]> sqlArgs = (List<Object[]>) params.stream()
                .map(p -> Arrays.stream(fields)
                        .map(f -> {
                            f.setAccessible(true);
                            Object value = null;
                            try {
                                value = f.get(p);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                            return value;
                        }).toArray(Object[]::new))
                .collect(Collectors.toList());

        JdbcParam jdbcParam = new JdbcParam(StrUtil.format(sqlDefinition.getDialectSql(),
                strFields, getPlaceHolder(fields.length)), sqlArgs);
        jdbcParam.setBatch(true);
        return jdbcParam;
    }

    public static JdbcParam simpleUpdateFormat(SqlDefinition sqlDefinition, Object[] args) {
        final Signature signature = sqlDefinition.getSignature();
        final SqlMetadata sqlMetadata = sqlDefinition.getSelectedSqlMetadata();
        Object paramObj = convertArgsToSqlCommandParam(signature.getParams(), signature.getMethod(), args);
        Field[] fields = ClassUtil.getDeclaredFields(paramObj.getClass());
        String strFields = null;
        String strIndexs = null;
        List<Object> sqlArgs = new ArrayList<>();

        Map<Boolean, List<Field>> fieldsMap
                = Arrays.stream(fields).collect(Collectors.groupingBy(f -> f.isAnnotationPresent(UK.class)));
        if (sqlMetadata.isSelective()) {
            strFields = fieldsMap.get(false).stream().
                    filter(x -> {
                        Object value = ReflectUtil.getFieldValue(paramObj, x);
                        if (value != null) {
                            sqlArgs.add(value);
                            return true;
                        } else {
                            return false;
                        }
                    })
                    .map(x -> {
                        return StrUtil.toUnderlineCase(x.getName() + "=?");
                    })
                    .collect(Collectors.joining(","));
        } else {
            strFields = fieldsMap.get(false).stream().map(f -> {
                        sqlArgs.add(ReflectUtil.getFieldValue(paramObj, f));
                        return StrUtil.toUnderlineCase(f.getName() + "=?");
                    }
            ).collect(Collectors.joining(","));
        }

        strIndexs =
                fieldsMap.get(true).stream().map(f -> {
                    sqlArgs.add(ReflectUtil.getFieldValue(paramObj, f));
                    return StrUtil.toUnderlineCase(f.getName()) + "=?";
                }).collect(Collectors.joining(" AND "));
        return new JdbcParam(StrUtil.format(sqlDefinition.getDialectSql(),
                strFields, strIndexs), ArrayUtil.toArray(sqlArgs, Object.class));
    }

    public static JdbcParam simpleDeleteFormat(SqlDefinition sqlDefinition, Object[] args) {
        final Signature signature = sqlDefinition.getSignature();
        final SqlMetadata sqlMetadata = sqlDefinition.getSelectedSqlMetadata();
        Object paramObj = convertArgsToSqlCommandParam(signature.getParams(), signature.getMethod(), args);
        Field[] fields = ClassUtil.getDeclaredFields(paramObj.getClass());
        List<Object> sqlArgs = new ArrayList<>();
        String strIndexs = Arrays.stream(fields).filter(f -> f.isAnnotationPresent(UK.class))
                .map(f -> {
                    sqlArgs.add(ReflectUtil.getFieldValue(paramObj, f));
                    return StrUtil.toUnderlineCase(f.getName()) + "=?";
                }).collect(Collectors.joining(" AND "));
        return new JdbcParam(StrUtil.format(sqlDefinition.getDialectSql(), strIndexs),
                ArrayUtil.toArray(sqlArgs, Object.class));
    }

    public static JdbcParam simpleSelectFormat(SqlDefinition sqlDefinition, Object[] args) {
        final Signature signature = sqlDefinition.getSignature();
        final SqlMetadata sqlMetadata = sqlDefinition.getSelectedSqlMetadata();
        Object paramObj = convertArgsToSqlCommandParam(signature.getParams(), signature.getMethod(), args);
        Field[] fields = ClassUtil.getDeclaredFields(paramObj.getClass());
        List<Object> sqlArgs = new ArrayList<>();
        String strIndexs = Arrays.stream(fields).filter(f -> f.isAnnotationPresent(UK.class))
                .map(f -> {
                    sqlArgs.add(ReflectUtil.getFieldValue(paramObj, f));
                    return StrUtil.toUnderlineCase(f.getName()) + "=?";
                }).collect(Collectors.joining(" AND "));
        return new JdbcParam(StrUtil.format(sqlDefinition.getDialectSql(), strIndexs),
                ArrayUtil.toArray(sqlArgs, Object.class));
    }

    public static JdbcParam simplePageQueryFormat(SqlDefinition sqlDefinition, Object[] args) {
        final Signature signature = sqlDefinition.getSignature();
        PageParam pageParam = new PageParam(1, 1000);
        if (signature.getPageParamIndex() != null) {
            pageParam = signature.extractPageParam(args);
        }
        JdbcParam jdbcParam = new JdbcParam(sqlDefinition.getDialectSql(), sqlDefinition.getPageQueryCountSql(),
               null);
        jdbcParam.setPage(true);
        jdbcParam.setPageParam(pageParam);
        jdbcParam.setJdbcPageArgs(sqlDefinition.getPageParamFunction().apply(pageParam));
        return jdbcParam;

    }

    private static Object convertArgsToSqlCommandParam(SortedMap<Integer, String> params, Method method, Object[] args) {
        final int paramCount = params.size();
        if (args == null || paramCount == 0) {
            return null;
        } else if (paramCount == 1) {
            return args[params.keySet().iterator().next()];
        } else {
            final Map<String, Object> param = new HashMap<>();
            int i = 0;
            for (Map.Entry<Integer, String> entry : params.entrySet()) {
                param.put(entry.getValue(), args[entry.getKey()]);
                final String genericParamName = method.getParameters()[entry.getKey()].getName();
                if (!param.containsKey(genericParamName)) {
                    param.put(genericParamName, args[entry.getKey()]);
                }
                i++;
            }
            return param;
        }
    }

    private static String getPlaceHolder(int count) {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < count; i++) {
            str.append("?");
            if (i < count - 1) {
                str.append(",");
            }
        }
        return str.toString();
    }
}
